﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Threading;
using Microsoft.Win32;
using System.IO;
using System.Collections.Specialized;
using System.Xml.Linq;
using IOPath = System.IO.Path ;
using System.Windows.Resources;
using System.Xml;
using System.Diagnostics;

// TODO: Allow multiple selection for uploads
// TODO: Add queuing for upload
// TODO: Clean up code a lot ....

namespace WPFStorageClient
{

    using Microsoft.Samples.ServiceHosting.StorageClient;


    /// <summary>
    /// My main window class
    /// </summary>
    public partial class Window1 : Window, INotifyPropertyChanged
    {
        private Object SyncObject = new Object();           // object used to sync my container list.
        private static Window1 s_window1 = null ;
        
        // StorageClient types needed
        public StorageAccountInfo StorageAccountInfo { get; private set; }
        public BlobStorage BlobStorage { get; private set; }

        // Status text displayed at the bottom of the main window.
        private String _strStatusText = null ;
        public String StatusText
        {
            get
            {
                return _strStatusText;
            }
            set
            {
                if (String.Compare(value, _strStatusText, false, CultureInfo.InvariantCulture) != 0)
                {
                    _strStatusText = value  ;
                    NotifyPropertyChanged("StatusText");
                }
            }
        }

        //
        // EnableAccountEdit - bool used to enable/disable the edit box on the account management tab.
        //   I should look at using a multi trigger with length of both text boxes.
        //
        private bool _enableAccountEdit ;
        public bool EnableAccountEdit
        {
            get
            {
                return _enableAccountEdit;
            }
            set
            {
                if (value != _enableAccountEdit)
                {
                    _enableAccountEdit = true;
                    NotifyPropertyChanged("EnableAccountEdit");
                }
            }
        }

        // List of blob containers
        public ObservableCollection<Container> Containers {get; set; }

        // Parse storate account info used to enable connect. This is read from StorageAccountInfo.xml
        // It can also now be edited using the account management tab
        public XElement StorageAccounts { get; internal set; }
        private bool _storageAccountsChanged = false;

        private static String s_AccountFileName = @"StorageAccountInfo.xml" ;

        // Current set of items being uploaded
        public ObservableCollection<UploadItem> UploadItems { get; set; }

        #region PropertyChangeRegion

        public event PropertyChangedEventHandler PropertyChanged;

        protected void NotifyPropertyChanged(String strPropertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(strPropertyName));
            }
        }
        #endregion

        public Window1()
        {
            // Save off window 1 so I can get error messages up.
            Debug.Assert(s_window1 == null);
            s_window1 = this;

            Containers = new ObservableCollection<Container>();
            UploadItems = new ObservableCollection<UploadItem>();
            InitializeComponent();
            DataContext = this;
            StatusText = "Choose a storage account to connect";

            using (Stream stream = File.Open(s_AccountFileName, FileMode.Open, FileAccess.Read))
            {
                XmlReader reader = XmlReader.Create(stream);
                StorageAccounts = XElement.Load(reader);
                stream.Close();
            }

#if DEBUG && TESTUPLOADROGRESS
            timer.Interval = new TimeSpan(0, 0, 5);
            timer.Tick += new EventHandler(timer_Tick);
            timer.Start();
#endif
        }



#if DEBUG && TESTUPLOADPROGRESS
        //
        // A Bunch of code here i used to test the upload progress
        //
        class BlobUploadStatusEventArgsTest : BlobUploadStatusEventArgs
        {

            public BlobUploadStatus Status { get; set; }
            public Object State { get; set; }
            public Int64 TotalBlocks { get; set; }
            public Int64 CompletedBlocks { get; set; }
            public Double Kbps { get; set; }
            public String BlobName { get; set; }
        }
        
        private System.Windows.Threading.DispatcherTimer timer = new System.Windows.Threading.DispatcherTimer();
        private Int32 iStart = 1;
        void timer_Tick(object sender, EventArgs e)
        {
            List<UploadItem> itemsToRemove = new List<UploadItem>();
            lock (UploadItems)
            {

                //
                // First of all run through all of the existing items and add 10 to the 
                //   PercentComplete
                //
                foreach (UploadItem item in UploadItems)
                {
                    item.PercentComplete += 10;
                    if (item.PercentComplete == 100)
                    {
                        itemsToRemove.Add(item);
                    }
                }
            }
            foreach (UploadItem item in itemsToRemove)
            {
                UploadItems.Remove(item);
            }
            BlobUploadStatusEventArgsTest arg = new BlobUploadStatusEventArgsTest();
            arg.Status = BlobUploadStatus.BlobUploadBegin;
            arg.BlobName = iStart.ToString() + ") Dude here is a blob...";
            iStart++;
            UploadCallback(arg);
        }
#endif

        #region STATUS_MESSAGE_REGION
        //
        // StatusText 
        //   The following code are ways to set the status text with parameters and format strings. They all call SetStatusTextImpl
        // which insures that the call is being made on the UI thread.
        //
        public void SetStatusTextImpl(String strText)
        {
            if (System.Windows.Application.Current.Dispatcher.Thread !=
                    System.Threading.Thread.CurrentThread)
            {
                System.Windows.Application.Current.Dispatcher.BeginInvoke(
                            new Action<String>(SetStatusText), new Object[] { strText });
                return;
            }
            StatusText = strText;
        }

        public static void SetStatusText(String strText)
        {
            Debug.Assert(s_window1 != null);
            s_window1.SetStatusTextImpl(strText);
        }

        public void SetStatusText(String strFormat, Object[] args)
        {
            String strMessage = String.Format(strFormat, args);
            SetStatusText(strMessage);
        }

        public void SetStatusText(String strFormat, Object o)
        {
            SetStatusText(strFormat, new Object[]{o});
        }

        public void SetStatusText(String strFormat, Object o1, Object o2)
        {
            SetStatusText(strFormat, new Object[]{o1, o2});
        }

        public void SetStatusText(String strFormat, Object o1, Object o2, Object o3)
        {
            SetStatusText(strFormat, new Object[]{o1, o2, o3});
        }
        #endregion

       
        /// <summary>
        /// _comboBox_SelectionChanged - 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Account_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            Containers.Clear();
            EnableUIElement(_accountSelectionComboBox, false);        // disable the combo box until after the connect goes
            //
            // Get the account info from the UI thread then farm off work to bg thread.
            //
            XElement selectedElement = _accountSelectionComboBox.SelectedItem as XElement;
            StorageAccountInfo =
                new StorageAccountInfo(new Uri(selectedElement.Attribute("blobstorageendpoint").Value),
                                        false,
                                        selectedElement.Attribute("accountnamevalue").Value,
                                        selectedElement.Attribute("accountsharedkey").Value);
            ThreadPool.QueueUserWorkItem(new WaitCallback(Connect));
        }

        /// <summary>
        /// EnableUIElement
        ///     Routine to enable a UIElement on the fg thread. This call to isEnabled can not be made on a bg thread.
        /// </summary>
        private void EnableUIElement(UIElement elem, bool enable)
        {
            if (System.Windows.Application.Current.Dispatcher.Thread !=
                    System.Threading.Thread.CurrentThread)
            {
                System.Windows.Application.Current.Dispatcher.BeginInvoke(
                            new Action<UIElement, bool>(EnableUIElement), new Object[] { elem, enable });
                return;
            }
            elem.IsEnabled = enable ;
        }

        /// <summary>
        /// Connect - This routine is called to connect to the currently selected account and update the containers list.
        /// </summary>
        /// <param name="o"></param>
        public void Connect(Object o)
        {
            // Once the user hits connect, enumerate the containers
            SetStatusText("Connecting...");
            BlobStorage = BlobStorage.Create(StorageAccountInfo);
            BlobStorage.RetryPolicy = RetryPolicies.RetryN(2, TimeSpan.FromMilliseconds(100));

            SetStatusText("Getting container list");
            // Get the containers and copy them.
            IEnumerable<BlobContainer> containers = BlobStorage.ListBlobContainers();
            SetContainers(containers);
            // once i have the containers add them to the tree view
            EnableUIElement(_accountSelectionComboBox, true);
        }

        /// <summary>
        /// SetContainers - this routine is called by connect to fill the top level of the 
        ///    tree view.
        /// </summary>
        /// <param name="containers"></param>
        public void SetContainers(IEnumerable<BlobContainer> containers)
        {
            if (System.Windows.Application.Current.Dispatcher.Thread !=
                    System.Threading.Thread.CurrentThread)
            {
                System.Windows.Application.Current.Dispatcher.BeginInvoke(
                            new Action<IEnumerable<BlobContainer>>(SetContainers), new Object[] { containers });
                return;
            }

            lock (SyncObject)
            {
                SetStatusText("Enumerating containers");
                Containers.Clear();
                if (containers == null)
                {
                    return;
                }
                // Get the containers and copy them.
                foreach (BlobContainer container in containers)
                {
                    Containers.Add(new Container(container));
                }
                SetStatusText("Done");

                //
                // Queue up a user work item to walk the containers and enumerate the blobs within them.
                //
                foreach (Container container in Containers)
                {
                    ThreadPool.QueueUserWorkItem(new WaitCallback(container.GetBlobData));
                }
            }
        }

        /// <summary>
        /// UploadCallback
        ///     Called by the blob upload coade when we get an update to the current upload.
        /// </summary>
        /// <param name="e"></param>
        private void UploadCallback(BlobUploadStatusEventArgs e)
        {
            if (System.Windows.Application.Current.Dispatcher.Thread !=
                    System.Threading.Thread.CurrentThread)
            {
                System.Windows.Application.Current.Dispatcher.BeginInvoke(
                            new Action<BlobUploadStatusEventArgs>(UploadCallback), new Object[] { e });
                return;
            }

            switch (e.Status)
            {
                case BlobUploadStatus.BlobUploadBegin:
                    {
                        SetStatusText("Beginning upload of {0}", e.BlobName);
                        UploadItems.Add(new UploadItem(e.BlobName));
                    }
                    break;
                case BlobUploadStatus.BlobUploadComplete:
                    {
                        Container container = e.State as Container;
                        if (container != null)
                        {
                            SetStatusText("Upload of {0} to container {1} complete",
                                            e.BlobName, container.ContainerName);
                            ThreadPool.QueueUserWorkItem(new WaitCallback(container.RefreshBlobData));
                        }
                        else
                        {
                            SetStatusText("Upload complete");
                        }
                        UploadItem uploadItem = 
                            UploadItems.FirstOrDefault<UploadItem>(
                            delegate(UploadItem item)
                            {
                                return String.Compare(item.FileName, e.BlobName, true) == 0 ;
                            }
                        );
                        if (uploadItem != null)
                        {
                            UploadItems.Remove(uploadItem);
                        }
                    }
                    break;
                case BlobUploadStatus.BlobUploadProgress:
                    {
                        Double d = (e.CompletedBlocks*100)/e.TotalBlocks ;
                        SetStatusText("Uploading blob {2}, {0:F0}% complete, rate = {1:F2} Kbps", d, e.Kbps, e.BlobName);
                        UploadItem uploadItem =
                            UploadItems.FirstOrDefault<UploadItem>(
                            delegate(UploadItem item)
                            {
                                return String.Compare(item.FileName, e.BlobName, true) == 0;
                            }
                        );
                        if (uploadItem != null)
                        {
                            uploadItem.PercentComplete = (Int32)d;
                        }
                    }
                    break;
            }
        }

        /// <summary>
        /// DoFileUpload -
        /// Worker routine to begin the upload of a blob.
        /// </summary>
        /// <param name="o"></param>
        private void DoFileUpload(Object o)
        {
            Object[] args = (Object[])o;
            Container container = (Container)args[0];
            String fullFileName = (String)args[1];
            String fileName = IOPath.GetFileName(fullFileName);

            //
            // Check to be sure the file does not already exist in the current container
            //
            if (container.BlobContainer.DoesBlobExist(fileName))
            {
                String strMessage = String.Format(
                    "The file {0} already exists in the container {1}, please delete existing blob.",
                    fileName, container.ContainerName);
                MessageBox.Show("File Already Exists",
                    strMessage, MessageBoxButton.OK, MessageBoxImage.Error);
                return;
            }

            //
            // otherwise upload the file.
            //
            BlobProperties blobProperties = new BlobProperties(fileName);
            using (FileStream fs = File.Open(fullFileName, FileMode.Open, FileAccess.Read))
            {
                container.BlobContainer.CreateBlob(blobProperties,
                                                    new BlobContents(fs),
                                                    true,
                                                    new BlobUploadStatusCallback(UploadCallback),
                                                    container);
            }
        }

        /// <summary>
        /// CreateContainer
        /// Create a new container
        /// </summary>
        /// <param name="o"></param>
        public void CreateContainer( Object o)
        {
            String strContainerName = (String)o;
            SetStatusText("Creating container {0}", new Object[]{strContainerName});
            try
            {
                BlobContainer container = BlobStorage.GetBlobContainer(strContainerName);
                NameValueCollection containerMetadata = new NameValueCollection();
                containerMetadata.Add("Name", strContainerName);
                //Create the container if it does not exist.
                container.CreateContainer(containerMetadata, ContainerAccessControl.Public);
                SetStatusText("Container created");
            }
            catch (System.Exception e)
            {
                // TODO - I really should not be catching all exceptions, but for now just show a message.
                MessageBox.Show(e.Message);
            }
            //
            // Once I create the container force an update of the UI.
            //
            IEnumerable<BlobContainer> containers = BlobStorage.ListBlobContainers();
            SetContainers(containers);
        }

        /// <summary>
        /// DeleteContainer
        ///     BG routine to delete a container.
        /// </summary>
        /// <param name="o"> This routine is passed the container name</param>
        public void DeleteContainer(Object o)
        {
            String strContainerName = (String)o;
            try
            {
                BlobContainer container = BlobStorage.GetBlobContainer(strContainerName);
                if (container != null)
                {
                    container.DeleteContainer();
                }
            }
            catch (System.Exception e)
            {
                // TODO - I really should not be catching all exceptions, but for now just show a message.
                MessageBox.Show(e.Message);
            }
            IEnumerable<BlobContainer> containers = BlobStorage.ListBlobContainers();
            SetContainers(containers);
        }


        /// <summary>
        /// UploadNewFile_Click
        ///     Routine called when the user wants to upload a new file
        /// </summary>
        /// <param name="sender">ignored</param>
        /// <param name="e">ignored</param>
        private void UpdloadNewFile_Click(object sender, RoutedEventArgs e)
        {
            Container container = _treeView.SelectedItem as Container;
            if (container != null)
            {
                SetStatusText("Beginning Upload of file");
                //
                // Ok - browse for a file to upload first.
                //
                OpenFileDialog ofd = new OpenFileDialog();
                ofd.CheckFileExists = true;
                ofd.Multiselect = false;
                if (ofd.ShowDialog() == true)
                {
                    // begin the upload 
                    String fullFileName = ofd.FileName;
                    ThreadPool.QueueUserWorkItem(new WaitCallback(DoFileUpload), new Object[] { _treeView.SelectedItem, fullFileName });
                }
            }
        }

        /// <summary>
        /// DeleteContainer_Click - click routine for deleting a container
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void DeleteContainer_Click(object sender, RoutedEventArgs e)
        {
            Container container = _treeView.SelectedItem as Container ;
            if (container != null)
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback(DeleteContainer), container.ContainerName);
            }
        }

        /// <summary>
        /// CreateContainer_Click - click routine for creating a container
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void CreateContainer_Click(object sender, RoutedEventArgs e)
        {
            ContainerName containerName = new ContainerName();
            if (containerName.ShowDialog() == true)
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback(CreateContainer), containerName.Name);
            }
        }

        /// <summary>
        /// RemoveBlobFromContainer -
        ///     Removes a blob from a container
        /// </summary>
        /// <param name="blob"></param>
        private void RemoveBlobFromContainer(Blob blob)
        {
            if (System.Windows.Application.Current.Dispatcher.Thread !=
                    System.Threading.Thread.CurrentThread)
            {
                System.Windows.Application.Current.Dispatcher.BeginInvoke(
                            new Action<Blob>(RemoveBlobFromContainer), new Object[] { blob });
                return;
            }
            //
            // Remove myself from the list
            //
            if (blob.Container.Blobs.Contains(blob))
            {
                blob.Container.Blobs.Remove(blob);
            }
        }

        /// <summary>
        /// DeleteBlob - bg routine for doing the blob delete. It deletes the blob and then updates the UI.
        /// </summary>
        /// <param name="o"></param>
        private void DeleteBlob(Object o)
        {
            Blob blob = (Blob)o;
            blob.Container.BlobContainer.DeleteBlob(blob.BlobName);
            RemoveBlobFromContainer(blob);
        }

        /// <summary>
        /// DeleteBlob -
        ///     Click routine for deleting a blob.
        /// </summary>
        /// <param name="o"></param>
        private void DeleteBlob_Click(object sender, RoutedEventArgs e)
        {
            Blob blob = (Blob)_listBox.SelectedItem;
            ThreadPool.QueueUserWorkItem(new WaitCallback(DeleteBlob), blob);
        }

        /// <summary>
        /// GetUriForBlob_Click - Copies the uri for the given blob to the clipboard
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void GetUriForBlob_Click(object sender, RoutedEventArgs e)
        {
            Blob blob = _listBox.SelectedItem as Blob;
            if (blob != null &&
                    blob.Uri != null)
            {
                String strUri = blob.Uri.ToString();
                Clipboard.SetText(strUri);
            }
        }

        /// <summary>
        /// TreeView_SelectedItemChanged - called when the user clicks on a new contianer. It is used to update the 
        ///   blobs.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            //
            // If I am a prefix, make sure I get my blob data
            //
            Container container = _treeView.SelectedItem as Container ;
            if ( container != null)
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback(container.GetBlobData));
            }
        }

        //
        // Account management code -
        //   The code below is used to manage the account management code. The accounts are listed in a datagrid and
        // the user is allowed to create new accounts or edit the current accounts. 
        // TODO - add a delete button.
        //
        private void _dataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            EnableAccountEdit = _dataGrid.SelectedItem != null;
        }

        private void DeleteAccountButton_Click(object sender, RoutedEventArgs e)
        {
            XElement elem = (XElement)_dataGrid.SelectedItem;
            elem.Remove();
            _storageAccountsChanged = true;
        }

        public void NewAccount_Click(object sender, RoutedEventArgs e)
        {
            EditAccountInfoDialog dlg = new EditAccountInfoDialog();
            if (dlg.ShowDialog() == true)
            {
                XElement newElem = new XElement("storageaccountinfo",
                        new XAttribute("accountnamevalue", dlg.AccountName),
                        new XAttribute("accountsharedkey", dlg.AccountSharedKey),
                        new XAttribute("blobstorageendpoint", @"http://blob.core.windows.net"),
                        new XAttribute("queuestorageendpoint", @"http://queue.core.windows.net"),
                        new XAttribute("tablestorageendpoint", @"http://table.core.windows.net"));
                StorageAccounts.Add(newElem);
                //StorageAccounts.Elements().Concat(new List<XElement>() { newElem });
                _storageAccountsChanged = true;
            }
            
        }

        public void EditButton_Click(object sender, RoutedEventArgs e)
        {
            EditAccountInfoDialog dlg = new EditAccountInfoDialog();
            XElement elem = (XElement)_dataGrid.SelectedItem;
            dlg.AccountName = elem.Attribute("accountnamevalue").Value;
            dlg.AccountSharedKey = elem.Attribute("accountsharedkey").Value;
            if (dlg.ShowDialog() == true)
            {
                // The user modified the info
                bool fModified = false;

                if (String.Compare(elem.Attribute("accountnamevalue").Value,
                                    dlg.AccountName, false, CultureInfo.InstalledUICulture) != 0)
                {
                    elem.Attribute("accountnamevalue").Value = dlg.AccountName;
                    fModified = true;
                }
                if (String.Compare(elem.Attribute("accountsharedkey").Value,
                                    dlg.AccountSharedKey, false, CultureInfo.InstalledUICulture) != 0)
                {
                    elem.Attribute("accountsharedkey").Value = dlg.AccountSharedKey;
                    fModified = true;
                }

                if (fModified)
                {
                    _storageAccountsChanged = true;
                }


                // If the user modified the current selection in the combo box then
                //   we need to modify that one also.
                if (fModified && _accountSelectionComboBox.SelectedItem == elem)
                {
                    // Force a reconnect...
                    Account_SelectionChanged(this, null);
                }
            }

        }

        private void _window1_Closed(object sender, EventArgs e)
        {
            try
            {
                // Once the window is closed, save out my account changes.
                if (_storageAccountsChanged)
                {
                    Stream stream = File.Open(s_AccountFileName, FileMode.Create, FileAccess.Write);
                    XmlWriter writer = XmlWriter.Create(stream);
                    StorageAccounts.Save(writer);
                    writer.Close();
                }
            }
            catch (System.Exception except)
            {
                Debug.WriteLine(except.Message);
            }
        }
    }
    //
    // UploadItem -
    // This represents an item in our upload list. We will update it as we get notify events. 
    //
    public class UploadItem : INotifyPropertyChanged
    {
        public UploadItem(String FileName)
        {
            this.FileName = FileName;
        }

        // FileName -
        //  Property - used as the display name. 
        //
        public String FileName { get; private set; }

        //
        // PercentComplete -
        //   Property used to display the percent complete in the progress dialog. 
        //
        private Int32 _percentComplete;
        public Int32 PercentComplete
        {
            get
            {
                return _percentComplete;
            }
            set
            {
                if (value != _percentComplete)
                {
                    _percentComplete = value;
                    NotifyPropertyChanged("PercentComplete");
                }
            }
        }

        #region PropertyChangeRegion
        // Implementation of Property change events.
        //
        public event PropertyChangedEventHandler PropertyChanged;

        protected void NotifyPropertyChanged(String strPropertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(strPropertyName));
            }
        }
        #endregion

    }

}
